'''
RPC Utilities

Provides pair of objects RPCRequest which work on client side and
RPCDispatcher on server side. They use information about server-side class, i.e.

class MathServer:
    def sum(a, b):
        return a + b
        
    def pow(a, p=2)
        return math.pow(a, p)

MathServer.rpc_info = RPCInfo()
MathServer.rpc_info.register(MathServer, 'sum', 'pow')

After that, objects of RPCClient translate calls to itself to dictionary:
    .pow(2) -> {'call': 'pow', 'data': {'a': 2, 'p': 2}}
This dictionaries may be sent across network

RPCDispatcher processes incoming data and makes appropriate call in service object
'''

import inspect

from salsa.util.io import IOErrorReply
from salsa.util import LoggerSeverity

import salsa
from salsa import trace_rpc, do_trace, do_trace_exception, SALSAException

class RPCException(SALSAException):
    pass

class RPCProtocol:
    KEY_CALL = 'call'
    KEY_DATA = 'data'
    
    KEY_ERROR_CODE = 'code'
    KEY_ERROR_INFO = 'info'
    
    STATUS_OK = 'OK'

class RPCError:
    '''Throwable class that injects error occured on server-side to client-side
    
    @todo: Dependent on IOErrorReply'''
    BAD_REQUEST = 10
    UNKNOWN_CALL = 11
    INTERNAL_ERROR = 12
    
    def __init__(self, status, info = {}):
        self.status = status
        self.info = info
    
    def get_reply(self):
        return IOErrorReply({RPCProtocol.KEY_ERROR_CODE: self.status, 
                             RPCProtocol.KEY_ERROR_INFO: self.info})

class RPCCall:
    '''Defines RPC call in RPCInfo class
    
    Exists on server-side and client-side both,
    but on client-side it is not bound to any real object
    and so cannot be called (only keeps info about call)'''
    def __init__(self, call, name, typeref):
        self.call = call
        self.name = name
        self.typeref = typeref
        self.obj = None
        
        self.argspec = inspect.getargspec(call)
        
        if self.argspec.varargs or self.argspec.keywords:
            raise "RPC Call couldn't have *args or **kwargs"
        
        self.args = filter(lambda arg: arg != 'self', self.argspec.args)
        # Convert self.argspec.defaults to dictionary
        if self.argspec.defaults:
            def_values = self.argspec.defaults
            self.defaults = dict(zip(self.argspec.args[-len(def_values):], def_values))
        else:
            self.defaults = {}
        
        try:
            self.checker = getattr(call, 'checker')
        except AttributeError:
            self.checker = None
            
        if trace_rpc: do_trace( 'RPCCall %s is created' % self.name)
    
    def bind(self, obj):
        '''Bind call from real server-side object to proxy calls to it'''
        if isinstance(obj, self.typeref):
            self.obj = obj
            
            if trace_rpc: do_trace( 'RPCCall %s is bound to %s' % (self.name, obj))
    
    def __str__(self):
        return "RPCCall %s(args: %s ; defaults: %s)" % (self.name, ", ".join(self.args), self.defaults)
      
    def __call__(self, **kwargs):
        '''Make call to server-side object'''
        if self.checker:
            if not self.checker(kwargs):
                return 'Bad request'
         
        if self.obj:   
            if trace_rpc: do_trace( 'RPCCall %s is called' % (self.name))
            return self.call(self.obj, **kwargs)
        
        raise TypeError('RPCCall %s not yet bound' % (self.name, ))
    
class RPCInfo:
    def __init__(self):
        self.call_key = RPCProtocol.KEY_CALL
        self.data_key = RPCProtocol.KEY_DATA
        
        self.calls = {}
    
    def register(self, typeref, *names):
        for name in names:
            if isinstance(name, tuple):
                alias = name[1]
                name = name[0]
            else:
                alias = name
                
            call = getattr(typeref, name)
            
            if not callable(call):
                raise ValueError("'proc' must be callable")
        
            self.calls[alias] = RPCCall(call, alias, typeref)
    
    def get_call(self, name):
        return self.calls[name]
    
    def __str__(self):
        s = 'RPCInfo <\n'
        
        for call in self.calls.values():
            s += str(call) + "\n"
        
        s += '>'
        
        return s
    
    def __iter__(self):
        return iter(self.calls.values())

class RPCClient:
    class Functor:
        '''Makes argument-mapping for call and sends dict to server using
        communicator comm
        
        @see salsa.util.io.IOSlaveCommunicator
        @see salsa.util.io.IOTransactionError'''
        def __init__(self, call, rpc_info, comm):
            self.rpc_info = rpc_info
            self.call = call
            self.comm = comm
        
        def __call__(self, *args, **kwargs):
            # Arrange positional args
            pos_args_num = len(args)
            max_args_num =  len(self.call.args)
            if pos_args_num > max_args_num:
                raise TypeError('%s() takes only %s arguments, %s given' % (self.call.name, max_args_num, pos_args_num))
            
            pos_args = zip(self.call.args[:pos_args_num], args)
            
            # Extended args
            ext_args_names = self.call.args[pos_args_num:]
            
            ext_args = []
            
            for arg_name in ext_args_names:
                if arg_name in kwargs:
                    value = kwargs[arg_name]
                elif arg_name in self.call.defaults:
                    value = self.call.defaults[arg_name]
                else:
                    raise TypeError('%s() missing argument %s' % (self.call.name, arg_name))
                    
                ext_args.append((arg_name, value))
            
            call_data = {self.rpc_info.call_key : self.call.name, 
                         self.rpc_info.data_key: dict(pos_args + ext_args)}            
            
            if trace_rpc: do_trace( 'RPCClient request %s id %d' % (repr(call_data), id(call_data)))
            
            reply = self.comm.communicate(call_data)
            
            if trace_rpc: do_trace( 'RPCClient request %d -> reply %s' % (id(call_data), repr(reply)))
            
            return reply
        
    def __init__(self, rpc_info, comm):       
        for call in rpc_info:
            if trace_rpc: do_trace( 'RPCClient call %s' % str(call))
            
            if call.name in dir(RPCClient):
                raise KeyError("%s is reserved name and couldn't be part of RPC")
            
            setattr(self, call.name, RPCClient.Functor(call, rpc_info, comm))
            

class RPCDispatcher:
    def __init__(self, rpc_info):
        self.rpc_info = rpc_info
    
    def bind(self, *objlist):
        for call in self.rpc_info:
            for obj in objlist:
                call.bind(obj)
            
            if not call.obj:
                raise TypeError('Incomplete binding: call %s (type: %s) was not bound to any object' % 
                                (call.name, call.typeref.__name__))
    
    def try_process(self, request):    
        if trace_rpc: do_trace( 'RPCDispatcher: request %s id %d' % (repr(request), id(request)))
            
        try:
            call_name = request[self.rpc_info.call_key]
            data = request[self.rpc_info.data_key]
        except:
            if trace_rpc: do_trace( 'RPCDispatcher: bad request %s' % (request))
            raise RPCError(RPCError.BAD_REQUEST)
        
        try:
            call = self.rpc_info.get_call(call_name)
        except Exception as e:
            if trace_rpc: do_trace( 'RPCDispatcher: unknown call %s' % (call_name))
            raise RPCError(RPCError.UNKNOWN_CALL, {'call': call_name, 'exc': str(e)})
        
        try:
            reply = call(**data)
        except Exception as e:
            salsa.logger.log(LoggerSeverity.LS_ALERT, 'Internal error %s during processing request ->%s' % (str(e), call_name))
            do_trace_exception('RPCDispatcher::try_process')
            if trace_rpc: do_trace( 'RPCDispatcher: internal error %s' % (str(e)))
            
            raise RPCError(RPCError.INTERNAL_ERROR, str(e)) 
        
        if trace_rpc: do_trace( 'RPCDispatcher: request %d -> reply %s' % (id(request), repr(reply)))
        
        return reply
        
    def process(self, request):
        try:
            reply = self.try_process(request)
        except RPCError as rpce:
            # Inject IOErrorReply
            return rpce.get_reply()
        
        return reply 